/*************************************************************************
	> 作者: zhaoyong  [2015.10.08]
	> 备注: 本程序用于将要备份的目录 与 网盘的自同步目录同步. 
			从网盘自同步目录删除的东西将放于回收站文件夹中
	> 编译: gcc app.c data.c -o app
 ************************************************************************/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
//#define _SVID_SOURCE
#include <dirent.h> //目录
#include "data.h"
#include <time.h>
#include <utime.h>	//utime

#define TYPE_DIR	4		//文件类型为目录
#define TYPE_FILE	8		//文件类型为普通文件
#define PATH_LEN	1024	//路径的最大长度

node_t *src_tree, *dest_tree;	//原目录, 同步的目录 数据结构指针

char recovery_path[PATH_LEN];	//回收站路径, 由main函数根据命令行参数提供

void perror_exit(const char *str)
{
	perror(str);
	exit(1);
}
//扫描path目录, 将目录项放入结构体中. 第一次调用时 flog = 1. 之后为 0 (用于将目录本身添加入结构体中)
void scan_a_dir(node_t **node, const char *path, int flog)
{
	struct dirent **name_list;
	int list_len = 0;
	char tmp_path[PATH_LEN];	//保存要扫描的子目录的路径
	int rtn = 0;	//保存各返回值

	 /*  扫描本目录, 得到目录下所有记录项*/
	list_len = scandir(path, &name_list, NULL, alphasort); //扫描目录
	if (list_len < 0) {
		perror_exit("scandir error");
	}
	int i;

	for (i = 0; i < list_len; i++) {	//遍历本目录的记录项
		char *file_name = name_list[i]->d_name; //保存文件名
		int file_type = name_list[i]->d_type;	//保存文件类型 4目录, 8普通文件
			/* 将不处理目录项中的 .. */
		if (strcmp(file_name, "..")) { 
			struct stat inode_info; //保存文件inode信息
			char tmp_file_name[512];
			sprintf(tmp_file_name, "%s/%s", path, file_name);	//拼接本文件的路径
				/* 读取本文件(目录)的 inode信息, 将最后修改时间放入本节点中*/
			if (lstat(tmp_file_name, &inode_info) < 0) { //读取inode信息
				perror("lstat error");
				goto FREE;
			}
				/* 因为根节点中名字需为传入路径本身, 则在第一次执行时 读取传入路径下的 . ,生成传入路径本身的节点 */
			if (!strcmp(file_name, ".")) { //要将第一次扫描的主目录添加
				if (flog == 1) {
					insert_node(node, file_type, path, inode_info.st_mtime); //插入一个node节点
				}
				continue;
			}
#if 1
			if (!strcmp(file_name, ".klive")){ //为 金山快盘的 工作文件夹
				if (flog == 1) {
				continue;	
				}
			}
#endif
					/* 当生成根节点之后, 将不生成 . 对应的节点, 而节点名称也为本条记录项中名字 */
			node_t *new_node = insert_node(node, file_type, file_name, inode_info.st_mtime); //插入一个node节点
			if (new_node == NULL) {
				perror("insert_node error");
				goto FREE;
			}
			/* 如果该记录项为目录, 则在生成目录本身的节点后, 需递归的生成该目录下所有文件(目录)的节点 */
			if (name_list[i]->d_type == 4) { //目录文件
				sprintf(tmp_path, "%s/%s", path, file_name); //拼接新路径
				scan_a_dir(&new_node, (const char *)tmp_path, 0);	//扫描子目录, 此时flog = 0
			}
		}
	}
FREE:	/* 用于执行调用失败后, 直接释放已开辟了的资源 */
	for (i = 0; i < list_len; i++) {	//释放name_list资源
		free(name_list[i]);
	}
	free(name_list);
}

//改变dest目录中所有文件的时间为 src目录中对应文件的时间
//eg: 要2/11/  --> 1/11 的时间, 则传入src_node: 为src目录中的头节点(直接子节点) src: 1/11 dest: 2/11
void change_dir_time(node_t *src_node, const char *src, const char *dest)
{
	node_t *tmp = src_node;
	 /* 遍历 src目录下的所有节点(因为此时经过文件夹的cp, src目录和dest目录的结构相同, 只是dest目录中文件时间被刷新了) */
	for (; tmp; tmp = tmp->r) { 

			/* 通过拼接, 生成本节点对应文件在 src, dest目录中的路径 */
		char new_path_src[PATH_LEN], new_path_dest[PATH_LEN];	//保存当前操作到文件的路径
		sprintf(new_path_src, "%s/%s", src, tmp->data->file_name);	//拼接, 生成文件路径
		sprintf(new_path_dest, "%s/%s", dest, tmp->data->file_name);

				/* 修改本节点在dest中对应文件的时间 */
		FILE *time_fd;
		char time_cmd[PATH_LEN+PATH_LEN];
		sprintf(time_cmd, "touch -r %s %s", new_path_src, new_path_dest);	//将__dest 的时间修改为 __src 的时间
		time_fd = popen(time_cmd, "r");
		pclose(time_fd);

				/* 若本节点为目录, 则还需递归的修改本目录下文件的时间 */
		if (tmp->data->type == TYPE_DIR) { //是目录
			change_dir_time(tmp->c, new_path_src, new_path_dest);  //传入本节点的直接子节点, 直接子节点的路径前缀
		}
	}
}
#define MODE_CP	1	//将 src中文件 cp 到 dest对应路径中
#define MODE_MV 2	//将 dest中对应 mv 到 回收站文件夹中
//执行从src_path/filename --> dest_path 的对应操作
//参数: mode为模式(cp, mv) type为源文件类型(4, 8) log为日志文件的指针
//返回: 成功0, 失败, 负数的错误码
//错误码: -1:popen调用失败, -2: mode参数错误, -3:type参数错误
int action(int mode, int type, node_t *src_node, const char *src_path, const char *dest_path, FILE *log)
{
	static int counter = 1;	//执行程序计数, 在打印log时所用
	char *filename = src_node->data->file_name;	//原节点文件名
	char cmd[PATH_LEN]; //保存命令
	char tmp_str[PATH_LEN];			//拼接要操作文件名与操作路径
	char tmp_dest_str[PATH_LEN+PATH_LEN];	//拼接执行命令后, 产生新文件的路径

	sprintf(tmp_str, "%s/%s", src_path, filename); //拼接	
	if (mode == MODE_CP) { // cp
		if (type == TYPE_FILE) {
			sprintf(cmd, "cp %s %s", tmp_str, dest_path);
		} else if (type == TYPE_DIR) {
			sprintf(cmd, "cp -r %s %s",tmp_str, dest_path);
		} else {
			return -3;
		}
	} else if (mode == MODE_MV) { //要将 src_parh mv 到 dest_path(回收站文件夹)
			sprintf(cmd, "mv -i %s %s",tmp_str, dest_path);
	} else { //参数传入错误
		return -2;
	}
			/* 写日志文件*/
	if (counter == 1) { //第一次执行, 额外打印
		time_t now_time = time(NULL); //将ctime取得的字符串的最后的\n去掉
		char *str = ctime(&now_time); 
		char now_time_str[PATH_LEN];
		strcpy(now_time_str, str);
		now_time_str[strlen(str)-1] = '\0';
		fprintf(log, "----------------------------------time[%s]---------------\n", now_time_str);
	} 
	fprintf(log, "[%d] CMD: [%s]\n", counter, cmd);		//将操作写入日志文件
	fflush(log);

		/* 执行操作 */
	FILE *read = popen(cmd, "r"); //执行cmd, 并用read指针读取执行结果
	if (read == NULL) {
		perror("popen error");
		return -1;
	}
	pclose(read); //等待popen启动的进程结束

		/* 执行cp后, 在目标路径生成新的文件(目录). 但新生成的时间被刷新为现在的时间. 
		 * 为和原路径中的对应一致, 需修改新文件(目录)的时间 
		 * 而执行mv, 将文件放入回收站之后, 则无需修改时间*/
	if (mode == MODE_CP) {
		sprintf(tmp_dest_str, "%s/%s", dest_path, filename);
		FILE *time_fd;	
		char time_cmd[PATH_LEN];
		sprintf(time_cmd, "touch -r %s %s", tmp_str, tmp_dest_str);
		time_fd = popen(time_cmd, "r");	//执行操作
		pclose(time_fd); //等待操作完成后返回
				/* 若该文件为目录,在修改完目录本身后, 需修改目录中文件的时间 */
		if (src_node->data->type == TYPE_DIR) { 
			//修改该目录下的文件(目录)的对应时间. 传入该目录下的直接子节点, 目录的路径
			change_dir_time(src_node->c, tmp_str, tmp_dest_str); 
		}
	}

	counter++;		//本程序执行次数++, 用于打印log
	return 0;
}

//比较两目录的结构, 执行相应操作. 操作后 src == dest 且不会修改src中的数据
//传入src的数据树, dest的数据树, src目录路径, dest目录路径, log文件指针(用于打印日志)
void dir_cmp(node_t *src, node_t *dest, const char *src_path, const char *dest_path, FILE *log)
{
	node_t *tmp_s = src, *tmp_d = dest;	//遍历因子
	int rtn = 0;
	for (;;) {
		if (tmp_s==NULL && tmp_d==NULL) { //比较完成
			return ;
		} else if (tmp_s == NULL) { //仅遍历完原目录
			// 全部 mv --> 回收站文件夹中
			node_t *tmp = tmp_d;
			for (; tmp; tmp = tmp->r) {
				action(MODE_MV, tmp->data->type, tmp, dest_path, recovery_path, log); //将dest多余文件(文件夹)移动到回收站文件夹中
			}
			return ;	
		} else if (tmp_d == NULL) { //仅遍历完目标目录
			// 全部 cp --> 目标文件夹中
			node_t *tmp = tmp_s;
			for (; tmp; tmp = tmp->r) {
				action(MODE_CP, tmp->data->type, tmp, src_path, dest_path, log); //将src多余文件 cp 到目标文件夹中
			}
			return ;
		} else { //均未遍历完
			rtn = strcmp(tmp_s->data->file_name, tmp_d->data->file_name);
			if (rtn == 0) { //两个节点表示为同一文件
				if (tmp_s->data->type==TYPE_DIR && tmp_d->data->type==TYPE_DIR) { //均为目录
					char tmp_src_p[PATH_LEN], tmp_dest_p[PATH_LEN];
					sprintf(tmp_src_p, "%s/%s", src_path, tmp_s->data->file_name);	//拼接下一级目录路径
					sprintf(tmp_dest_p, "%s/%s", dest_path, tmp_d->data->file_name);
					dir_cmp(tmp_s->c, tmp_d->c, tmp_src_p, tmp_dest_p, log); //递归比较子目录
				} else if (tmp_s->data->type==TYPE_FILE && tmp_d->data->type==TYPE_FILE){ //不是目录
					if (difftime(tmp_s->data->time, tmp_d->data->time)) { //两时间不同
						// cp src_path --> dest_path
						action(MODE_CP, tmp_s->data->type, tmp_s, src_path, dest_path, log); //将改变了的文件从 src cp 到目标文件夹中
					}
				}
				tmp_s = tmp_s->r;
				tmp_d = tmp_d->r;
			} else if (rtn < 0) { //src中增加文件 or dest中减少文件
				//将src中对应文件 ---cp--> dest
				action(MODE_CP, tmp_s->data->type, tmp_s, src_path, dest_path, log); // cp src文件 --> dest
				tmp_s = tmp_s->r;
			} else { //src中减少了文件 or dest中增加了文件
				//将dest中对应文件 ---mv--->回收站文件夹 
				action(MODE_MV, tmp_d->data->type, tmp_d, dest_path, recovery_path, log); //将dest 中多余文件(夹)移动到回收站文件夹中
				tmp_d = tmp_d->r;
			}
		}
	}
}
int main(int argc, const char *argv[])
{
	if (argc < 5) {
		printf("arg error: eg: ./a.out  src_path  dest_path   file_log  recovery_path\n");
		printf("arg error: 示例: 本程序名 要备份的路径 网盘自动备份的路径 日志文件的路径(无则创建) 回收站路径(必须存在)\n");
		exit(1);
	}
	FILE *log = NULL;	//日志文件
	log = fopen(argv[3], "a+");	//以追加权限打开该日志文件

	strcpy(recovery_path, argv[4]);	//填写全局变量 回收站路径

	scan_a_dir(&src_tree, argv[1], 1);	//扫描 源目录, 将目录结构保存于数据结构 src_tree 中
	scan_a_dir(&dest_tree, argv[2], 1);	//扫描 目标目录, 将目录结构保存于数据结构 dest_tree 中

#if 0
	print_tree(src_tree, 0);			//打印树
	printf("--------------------------\n");
	print_tree(dest_tree, 0);
	printf("-----------------------------------------------------\n");
#endif

	dir_cmp(src_tree->c, dest_tree->c, argv[1], argv[2], log); //比较两个数据结构, 传入对应路径, log文件指针
	

	fprintf(log, "----------------------------------END---------------\n");
	fclose(log);	//关闭log文件

	src_tree = delete_tree(src_tree);			//删除树
	dest_tree = delete_tree(dest_tree);
	return 1;
}
